package com.github.marschall.threeten.jpa.oracle.impl; import static java.lang.Byte.toUnsignedInt; import static java.time.ZoneOffset.UTC; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; import oracle.sql.TIMESTAMP; import oracle.sql.TIMESTAMPTZ; import oracle.sql.ZONEIDMAP; /** * Converts between {@link TIMESTAMPTZ}/{@link TIMESTAMP} and java.time times and back. */ public final class TimestamptzConverter { private static final int INV_ZONEID = -1; // magic private static final int SIZE_TIMESTAMPTZ = 13; private static final int SIZE_TIMESTAMP = 11; private static final int OFFSET_HOUR = 20; private static final int OFFSET_MINUTE = 60; private static final byte REGIONIDBIT = (byte) 0b1000_0000; // -128 // Byte 0: Century, offset is 100 (value - 100 is century) // Byte 1: Decade, offset is 100 (value - 100 is decade) // Byte 2: Month UTC // Byte 3: Day UTC // Byte 4: Hour UTC, offset is 1 (value-1 is UTC hour) // Byte 5: Minute UTC, offset is 1 (value-1 is UTC Minute) // Byte 6: Second, offset is 1 (value-1 is seconds) // Byte 7: nanoseconds (most significant bit) // Byte 8: nanoseconds // Byte 9: nanoseconds // Byte 10: nanoseconds (least significant bit) // Byte 11: Hour UTC-offset of Timezone, offset is 20 (value-20 is UTC-hour offset) // Byte 12: Minute UTC-offset of Timezone, offset is 60 (value-60 is UTC-minute offset) /** * Converts {@link OffsetDateTime} to {@link TIMESTAMPTZ}. * * @param attribute the value to be converted, possibly {@code null} * @return the converted data, possibly {@code null} */ public static TIMESTAMPTZ offsetDateTimeToTimestamptz(OffsetDateTime attribute) { if (attribute == null) { return null; } byte[] bytes = newTimestamptzBuffer(); ZonedDateTime utc = attribute.atZoneSameInstant(UTC); writeDateTime(bytes, utc.toLocalDateTime()); ZoneOffset offset = attribute.getOffset(); writeZoneOffset(bytes, offset); return new TIMESTAMPTZ(bytes); } /** * Converts {@link TIMESTAMPTZ} to {@link OffsetDateTime}. * * @param dbData the data from the database to be converted, possibly {@code null} * @return the converted value, possibly {@code null} */ public static OffsetDateTime timestamptzToOffsetDateTime(TIMESTAMPTZ dbData) { if (dbData == null) { return null; } byte[] bytes = dbData.toBytes(); OffsetDateTime utc = extractUtc(bytes); if (isFixedOffset(bytes)) { ZoneOffset offset = extractOffset(bytes); return utc.withOffsetSameInstant(offset); } else { ZoneId zoneId = extractZoneId(bytes); return utc.atZoneSameInstant(zoneId).toOffsetDateTime(); } } /** * Converts {@link ZonedDateTime} to {@link TIMESTAMPTZ}. * * @param attribute the value to be converted, possibly {@code null} * @return the converted data, possibly {@code null} */ public static TIMESTAMPTZ zonedDateTimeToTimestamptz(ZonedDateTime attribute) { if (attribute == null) { return null; } byte[] bytes = newTimestamptzBuffer(); ZonedDateTime utc = attribute.withZoneSameInstant(UTC); writeDateTime(bytes, utc.toLocalDateTime()); String zoneId = attribute.getZone().getId(); int regionCode = ZONEIDMAP.getID(zoneId); if (isValidRegionCode(regionCode)) { writeZoneId(bytes, regionCode); } else { writeZoneOffset(bytes, attribute.getOffset()); } return new TIMESTAMPTZ(bytes); } /** * Converts {@link TIMESTAMPTZ} to {@link ZonedDateTime}. * * @param dbData the data from the database to be converted, possibly {@code null} * @return the converted value, possibly {@code null} */ public static ZonedDateTime timestamptzToZonedDateTime(TIMESTAMPTZ dbData) { if (dbData == null) { return null; } byte[] bytes = dbData.toBytes(); OffsetDateTime utc = extractUtc(bytes); if (isFixedOffset(bytes)) { ZoneOffset offset = extractOffset(bytes); return utc.atZoneSameInstant(offset); } else { ZoneId zoneId = extractZoneId(bytes); return utc.atZoneSameInstant(zoneId); } } /** * Converts {@link LocalDateTime} to {@link TIMESTAMP}. * * @param attribute the value to be converted, possibly {@code null} * @return the converted data, possibly {@code null} */ public static TIMESTAMP localDateTimeToTimestamp(LocalDateTime attribute) { if (attribute == null) { return null; } byte[] bytes = newTimestampBuffer(); writeDateTime(bytes, attribute); return new TIMESTAMP(bytes); } /** * Converts {@link TIMESTAMP} to {@link LocalDateTime}. * * @param dbData the data from the database to be converted, possibly {@code null} * @return the converted value, possibly {@code null} */ public static LocalDateTime timestampToLocalDateTime(TIMESTAMP dbData) { if (dbData == null) { return null; } byte[] bytes = dbData.toBytes(); return extractLocalDateTime(bytes); } private static LocalDateTime extractLocalDateTime(byte[] bytes) { int year = ((toUnsignedInt(bytes[0]) - 100) * 100) + (toUnsignedInt(bytes[1]) - 100); int month = bytes[2]; int dayOfMonth = bytes[3]; int hour = bytes[4] - 1; int minute = bytes[5] - 1; int second = bytes[6] - 1; int nanoOfSecond = toUnsignedInt(bytes[7]) << 24 | toUnsignedInt(bytes[8]) << 16 | toUnsignedInt(bytes[9]) << 8 | toUnsignedInt(bytes[10]); return LocalDateTime.of(year, month, dayOfMonth, hour, minute, second, nanoOfSecond); } private static OffsetDateTime extractUtc(byte[] bytes) { return OffsetDateTime.of(extractLocalDateTime(bytes), UTC); } private static boolean isFixedOffset(byte[] bytes) { return (bytes[11] & REGIONIDBIT) == 0; } private static ZoneOffset extractOffset(byte[] bytes) { return ZoneOffset.ofHoursMinutes(bytes[11] - 20, bytes[12] - 60); } private static ZoneId extractZoneId(byte[] bytes) { // high order bits int regionCode = (bytes[11] & 0b1111111) << 6; // low order bits regionCode += (bytes[12] & 0b11111100) >> 2; String regionName = ZONEIDMAP.getRegion(regionCode); return ZoneId.of(regionName); } private static boolean isValidRegionCode(int regionCode) { return regionCode != INV_ZONEID; } private static byte[] newTimestamptzBuffer() { return new byte[SIZE_TIMESTAMPTZ]; } private static byte[] newTimestampBuffer() { return new byte[SIZE_TIMESTAMP]; } private static void writeDateTime(byte[] bytes, LocalDateTime utc) { int year = utc.getYear(); bytes[0] = (byte) (year / 100 + 100); bytes[1] = (byte) (year % 100 + 100); bytes[2] = (byte) utc.getMonthValue(); bytes[3] = (byte) utc.getDayOfMonth(); bytes[4] = (byte) (utc.getHour() + 1); bytes[5] = (byte) (utc.getMinute() + 1); bytes[6] = (byte) (utc.getSecond() + 1); int nano = utc.getNano(); bytes[7] = (byte) (nano >> 24); bytes[8] = (byte) (nano >> 16 & 0xFF); bytes[9] = (byte) (nano >> 8 & 0xFF); bytes[10] = (byte) (nano & 0xFF); } private static void writeZoneOffset(byte[] bytes, ZoneOffset offset) { int totalMinutes = offset.getTotalSeconds() / 60; bytes[11] = (byte) ((totalMinutes / 60) + OFFSET_HOUR); bytes[12] = (byte) ((totalMinutes % 60) + OFFSET_MINUTE); } private static void writeZoneId(byte[] bytes, int regionCode) { bytes[11] = (byte) (REGIONIDBIT | (regionCode & 0b1111111000000) >>> 6); bytes[12] = (byte) ((regionCode & 0b111111) << 2); } private TimestamptzConverter() { throw new AssertionError("not instantiable"); } }